package com.pt25.base.dep.shardingjdbc;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.pt25.base.dep.shardingjdbc.constant.ClusterConstant;
import com.pt25.base.dep.shardingjdbc.datasource.MultiDatasource;
import com.pt25.base.dep.shardingjdbc.datasource.MultiDatasourceContext;
import com.pt25.base.dep.shardingjdbc.properties.DbAddress;
import com.pt25.base.dep.shardingjdbc.properties.DbProperty;
import com.pt25.base.dep.shardingjdbc.properties.DbPropertyList;
import com.pt25.base.dep.shardingjdbc.properties.DbShardingRule;
import com.pt25.base.dep.shardingjdbc.transaction.MultiDatasourceTransactionFactory;
import com.pt25.base.exception.BizException;
import com.pt25.base.util.CollectionUtil;
import com.pt25.base.util.StrUtil;
import jakarta.annotation.Resource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.apache.shardingsphere.driver.api.ShardingSphereDataSourceFactory;
import org.apache.shardingsphere.infra.config.RuleConfiguration;
import org.apache.shardingsphere.infra.config.algorithm.ShardingSphereAlgorithmConfiguration;
import org.apache.shardingsphere.readwritesplitting.api.ReadwriteSplittingRuleConfiguration;
import org.apache.shardingsphere.readwritesplitting.api.rule.ReadwriteSplittingDataSourceRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.strategy.sharding.StandardShardingStrategyConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.*;

/**
 * 数据源核心配置类，支持单机，主从，集群数据库配置
 */
@Configuration
@ComponentScan
public class DbConfig {

    /**
     * 分页组件
     */
    @Resource
    private MybatisPlusInterceptor mybatisPlusInterceptor;

    /**
     * 多数据源配置列表
     */
    @Resource(name = "dbPropertyList")
    private DbPropertyList dbPropertyList;

    /**
     * 指定mybatis的mapper.xml的位置
     */
    @Value("${db-configs.mapperLocations:}")
    private String mapperLocations;

    /**
     * 定义实体别名(**Mapper.xml中实体类别名)，实体扫描，多个package用逗号或者分号分隔
     */
    @Value("${db-configs.typeAliasesPackage:}")
    private String typeAliasesPackage;


    /**
     * mybatis核心工厂
     */
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        //mybatis sqlSessionFactory设置数据源
        sqlSessionFactory.setDataSource(multipleDataSource());
        // 指定mybatis的mapper.xml的位置
        sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(StrUtil.isBlank(mapperLocations) ? "classpath*:/mapper/**/*Mapper.xml" : mapperLocations));
        // 定义实体别名(**Mapper.xml中实体类别名)，实体扫描，多个package用逗号或者分号分隔
        sqlSessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        // 添加事务配置 zcy
        sqlSessionFactory.setTransactionFactory(new MultiDatasourceTransactionFactory());
        // mybatis一些优化配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        configuration.setCallSettersOnNulls(true);
        sqlSessionFactory.setConfiguration(configuration);
        sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()
                mybatisPlusInterceptor //添加分页功能
        });
        //sqlSessionFactory.setGlobalConfig(globalConfiguration());
        return sqlSessionFactory.getObject();
    }

    @Bean
    @Primary
    public DataSource multipleDataSource() throws SQLException {
        MultiDatasource multiDataSource = new MultiDatasource();
        if (null != dbPropertyList.getConfigs() && checkDataSourceConfig()) {
            DbProperty primaryDbProperty = null;
            Map<Object, Object> targetDataSources = new HashMap<>();
            for (DbProperty dbProperty : dbPropertyList.getConfigs()) {
                if (dbProperty.getDatasourceType().equals(ClusterConstant.SINGLE)) {
                    targetDataSources.put(dbProperty.getDatasourceName(), createSingleDataSource(dbProperty));
                } else {
                    targetDataSources.put(dbProperty.getDatasourceName(), createDatasource(dbProperty));
                }
                if (dbProperty.getDatasourceName().equals(dbPropertyList.getPrimary())) {
                    primaryDbProperty = dbProperty;
                }
            }
            if (null != primaryDbProperty) {
                MultiDatasourceContext.setDefaultDatasource(primaryDbProperty.getDatasourceName());
                multiDataSource.setDefaultTargetDataSource(targetDataSources.get(primaryDbProperty.getDatasourceName()));
            }
            //添加数据源
            multiDataSource.setTargetDataSources(targetDataSources);
        }
        return multiDataSource;
    }

    public DataSource createDatasource(DbProperty dbProperty) throws SQLException {
        // 配置数据源
        Map<String, DataSource> dataSourceMap = createDataSourceMap(dbProperty);
        // 配置读写分离规则
        ReadwriteSplittingRuleConfiguration readwriteSplittingRuleConfig = createReadwriteSplittingRuleConfiguration(dbProperty);
        // 配置分片规则
        List<ShardingRuleConfiguration> shardingRuleConfigurations = createShardingRuleConfigurations(dbProperty);
        //合并读写分离和分片规则
        List<RuleConfiguration> ruleConfigurations = new ArrayList<>();
        if (null != shardingRuleConfigurations) ruleConfigurations.addAll(shardingRuleConfigurations);
        if (null != readwriteSplittingRuleConfig) ruleConfigurations.add(readwriteSplittingRuleConfig);
        // 创建数据源
        return ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, ruleConfigurations, new Properties());
    }

    private Map<String, DataSource> createDataSourceMap(DbProperty dbProperty) {
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        for (DbAddress dbAddress : dbProperty.getAddressList()) {
            dataSourceMap.put(dbAddress.getDbname(), createDataSource(dbProperty, dbAddress.getUrl()));
            if (CollectionUtil.isNotEmpty(dbAddress.getSlaveList())) {
                for (DbAddress slaveDbAddress : dbAddress.getSlaveList()) {
                    dataSourceMap.put(slaveDbAddress.getDbname(), createDataSource(dbProperty, slaveDbAddress.getUrl()));
                }
            }
        }
        return dataSourceMap;
    }

    private ReadwriteSplittingRuleConfiguration createReadwriteSplittingRuleConfiguration(DbProperty dbProperty) {
        if (ClusterConstant.MASTER_SLAVE.equals(dbProperty.getDatasourceType()) || ClusterConstant.CLUSTER_MASTER_SLAVE.equals(dbProperty.getDatasourceType())) {
            List<ReadwriteSplittingDataSourceRuleConfiguration> readwriteSplittingDataSourceRuleConfigurations = new ArrayList<>();
            for (DbAddress dbAddress : dbProperty.getAddressList()) {
                String masterDatabaseName = dbAddress.getDbname();
                List<String> slaveDatabaseNames = new ArrayList<>();
                if (CollectionUtil.isNotEmpty(dbAddress.getSlaveList())) {
                    for (DbAddress slaveDbAddress : dbAddress.getSlaveList()) {
                        slaveDatabaseNames.add(slaveDbAddress.getDbname());
                    }
                }
                ReadwriteSplittingDataSourceRuleConfiguration readwriteSplittingDataSourceRuleConfiguration =
                        new ReadwriteSplittingDataSourceRuleConfiguration(dbAddress.getDbname(), null, masterDatabaseName, slaveDatabaseNames, "round_robin");
                readwriteSplittingDataSourceRuleConfigurations.add(readwriteSplittingDataSourceRuleConfiguration);
            }
            if (CollectionUtil.isNotEmpty(readwriteSplittingDataSourceRuleConfigurations)) {
                return new ReadwriteSplittingRuleConfiguration(readwriteSplittingDataSourceRuleConfigurations,
                        Collections.singletonMap("round_robin", new ShardingSphereAlgorithmConfiguration("ROUND_ROBIN", new Properties())));
            }
        }
        return null;
    }

    private List<ShardingRuleConfiguration> createShardingRuleConfigurations(DbProperty dbProperty) {
        if (ClusterConstant.CLUSTER.equals(dbProperty.getDatasourceType()) || ClusterConstant.CLUSTER_MASTER_SLAVE.equals(dbProperty.getDatasourceType())) {
            List<ShardingRuleConfiguration> shardingRuleConfigurations = new ArrayList<>();
            if (CollectionUtil.isNotEmpty(dbProperty.getShardingRuleList())) {
                for (DbShardingRule dbShardingRule : dbProperty.getShardingRuleList()) {
                    if (StrUtil.isNotBlank(dbShardingRule.getShardingTable())) {
                        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
                        ShardingTableRuleConfiguration shardingTableRuleConfiguration = new ShardingTableRuleConfiguration(dbShardingRule.getShardingTable(), dbShardingRule.getShardingRule());
                        if (StrUtil.isNotBlank(dbShardingRule.getShardingTableColumn())) {
                            shardingTableRuleConfiguration.setTableShardingStrategy(new StandardShardingStrategyConfiguration(dbShardingRule.getShardingTableColumn(), "table_inline"));
                            shardingRuleConfig.getShardingAlgorithms().put("table_inline", new ShardingSphereAlgorithmConfiguration("INLINE", getTableShardingAlgorithmProperties(dbShardingRule.getShardingTableRule())));
                        }
                        if (StrUtil.isNotBlank(dbShardingRule.getShardingDbColumn())) {
                            StandardShardingStrategyConfiguration standardShardingStrategyConfiguration = new StandardShardingStrategyConfiguration(dbShardingRule.getShardingDbColumn(), "database_inline");
                            shardingRuleConfig.setDefaultDatabaseShardingStrategy(standardShardingStrategyConfiguration);
                            shardingRuleConfig.getShardingAlgorithms().put("database_inline", new ShardingSphereAlgorithmConfiguration("INLINE", getDatabaseShardingAlgorithmProperties(dbShardingRule.getShardingDbRule())));
                        }
                        shardingRuleConfig.getTables().add(shardingTableRuleConfiguration);
                        shardingRuleConfigurations.add(shardingRuleConfig);
                    }
                }
            }
            return shardingRuleConfigurations;
        }
        return null;
    }

    private static Properties getTableShardingAlgorithmProperties(String tableShardingAlgorithm) {
        Properties result = new Properties();
        result.setProperty("algorithm-expression", tableShardingAlgorithm);
        return result;
    }

    private static Properties getDatabaseShardingAlgorithmProperties(String databaseShardingAlgorithm) {
        Properties result = new Properties();
        result.setProperty("algorithm-expression", databaseShardingAlgorithm);
        return result;
    }


    private DataSource createSingleDataSource(DbProperty dbProperty) {
        return createDataSource(dbProperty, dbProperty.getAddressList().get(0).getUrl());
    }

    private DataSource createDataSource(DbProperty dbProperty, String url) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(dbProperty.getDriverClassName());
        dataSource.setUrl(url);
        dataSource.setUsername(dbProperty.getUsername());
        dataSource.setPassword(dbProperty.getPassword());
        dataSource.setInitialSize(dbProperty.getConnectionPool().getInitialSize());
        dataSource.setMaxActive(dbProperty.getConnectionPool().getMaxActive());
        dataSource.setMinIdle(dbProperty.getConnectionPool().getMinIdle());
        dataSource.setMaxWait(dbProperty.getConnectionPool().getMaxWait());
        dataSource.setPoolPreparedStatements(dbProperty.getConnectionPool().getPoolPreparedStatements());
        dataSource.setMaxOpenPreparedStatements(dbProperty.getConnectionPool().getMaxOpenPreparedStatements());
        return dataSource;
    }


    private boolean checkDataSourceConfig() {
        List<String> datasourceNameList = new ArrayList<>();
        for (DbProperty dbProperty : dbPropertyList.getConfigs()) {
            if (StrUtil.isBlank(dbProperty.getUsername()) || StrUtil.isBlank(dbProperty.getPassword())) {
                throw new BizException("数据库配置账号(username)或密码(password)不能为空！");
            }
            if (StrUtil.isBlank(dbProperty.getDatasourceName()) || StrUtil.isBlank(dbProperty.getDatasourceType())) {
                throw new BizException("数据库配置集群名称(datasourceName)或集群类型(datasourceType)不能为空！");
            }
            datasourceNameList.add(dbProperty.getDatasourceName());
        }
        Set<String> datasourceNameSet = new HashSet<>(datasourceNameList);
        if (datasourceNameSet.size() != datasourceNameList.size()) {
            throw new BizException("数据库配置集群名称(datasourceName)不可以重复！");
        }
        if (StrUtil.isNotEmpty(dbPropertyList.getPrimary()) && !datasourceNameSet.contains(dbPropertyList.getPrimary())) {
            throw new BizException("数据库配置默认数据源(primary)配置的集群名称(datasourceName)不存在！");
        }
        return true;
    }

}
